﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Collections;
using MessageServer.channels;

namespace MessageServer.channels.socket
{
    /**
     * Constants used by this layer. Add more constants here
     * */
    public class SocketConsts
    {
        public const int READ = 0x01;       //Sockets needed for READ
        public const int WRITE = 0x10;      //Sockets needed for WRITE
    }

    /**
     * Used to Indicate to the selector the operations to be performed on the channel. The user will
     * create a selection for the set of operations to be performed on the channel and register it with
     * a selector. This interface dictates the protocol to be implemented by the various selection key 
     * implementors.
     * */
    public interface SelectionKey
    {
        bool readable { get; }                  //Check if the channel is readable. If readable return true.
        bool writable { get; }                  //Check if the channel is writable. If writable return true.
        bool valid { get; }                     //Check if the channel is valid. If it is return true.
        int opers { get; set; }                 //set and return the operations that are active on this channel
        Selector selector { get; }              //return the selector that is operating on this key
        SelectableChannel channel { get; }      //return the channel for this key
        bool writeDataPresent { get; }          //return if data is present ot be written
    }

    /**
     * This implements the selector interface to check if data is available for the registered actions
     * on the channels registered with this selector. One selector can handle more than one channel. 
     * If a maximum limit on the number of channels handled needs to be set, it has to be implemented
     * by the implementing class. This interface does not limit it. This interface checks which of the 
     * registered keys are ready for the operation registered for and returns the number of channels that are ready. The 
     * selectedkeys provide the keys on which operations can be performed.
     * The same channel can be registered for read and write operations. Once the operations are completed, 
     * the doneSelect must be called to release the keys back to check if operation can be performed again
     * on it. The implementation for this class has to be thread safe and can be called from multiple threads.
     * */
    public interface Selector
    {
        Object attachment { get; set; }         //Any attachment that is common across keys and needs to be used
        void close();                           //Close this selector and all the related channels
        int select();                           //Wait till atleast one channel is ready for operation.
        int select(int timeout);                //Wait till timeout for one channel to be ready. If not return 0
        IList selectedKeys();                   //Return the selected keys that are available for operation
        void doneSelect(SelectionKey key);      //release the key to check for operation again.
    }

    /**
     * All channels that need to work with a selector and selectionkey has to implement this interface to be used
     * for checking if operations are available. 
     * */
    public interface SelectableChannel
    {
        void register(Selector selector, int oper);                     //register this channel with selector for oper
        void register(Selector selector, int oper, Object attachment);  //register this channel with selector for oper with attachment
        bool registered { get; }                                        //check if this channel is already registered
        SelectionKey key { get; }                                       //return the key for oper with this channel
        object attachment { get; set; }                                 //object to be attached to this key to retrieve later
        int channelid { get; }
    }

    /**
     * Implementation of the Selector to check SocketChannels are ready for operations READ and WRITE. This class
     * is a wrapper around the Sockets.select call and provides the required functionality against the SocketChannel.
     * THis class only operates on ClientSockets and cannot operate on ServerChannels.
     * The maximum keys this socket needs to be handled is passed as a construction parameter. Call the addChannel
     * and the removeChannel functions to register and deregister SocketChannels from this selector.
     * This class is not thread safe and has to be exclusively used in a single thread or the access locked.
     * */
    public class SocketSelector : Selector
    {
        Object _attachment;                 //attachment global to this selector
        Hashtable _selectRead;              //the registered selectionkeys for reading
        Hashtable _selectWrite;             //the registered selectiornkeys for writing
        Hashtable _allKeys;                 //all registered keys with this selector
        IList _selectedKeys;                //The keys that are selected by any previous select calls and not yet handled

        int _maxkeys;                       //maximum number of keys that will be handled by this selector
        int _id;                            //debugging purposes

        /**
         * The selector has to be initialized with the maximum number of keys that is
         * expected to be handled by this selector.
         * */
        public SocketSelector(int maxkeys, int id)
        {
            _maxkeys = maxkeys;
            _selectedKeys = new ArrayList();
            _id = id;
        }

        /**
         * Call this function to register the channel key to be looked at by this selector.
         * This function is a helper function used internal to this package. Use register function
         * on the channel to register the key. Do not call this function directly.
         * */
        internal void addChannel(SelectionKey key)
        {
            if (_selectRead == null)
            {
                _selectRead = new Hashtable(new SelectionKeyComparator(SocketConsts.READ));
                _selectWrite = new Hashtable(new SelectionKeyComparator(SocketConsts.WRITE));
                _allKeys = new Hashtable(new SelectionKeyComparator(0)); //don't compare operators
            }

            if (_allKeys.Count >= _maxkeys)
                ChannelLogger.LOG.LogDebug("Cannot handle more than " + _maxkeys + " keys");

            bool exists = addTo(key, SocketConsts.READ, _selectRead);
            if (exists) throw new Exception("Error already registered key.");
            exists = addTo(key, SocketConsts.WRITE, _selectWrite);
            if (exists) throw new Exception("Error already registered key.");
            addTo(key, 0, _allKeys); //egnore exists
            ChannelLogger.LOG.LogDebug("addChannel:Adding:" + key.channel.channelid + ":Read:" +
                _selectRead.Keys.Count + ":" + _selectWrite.Keys.Count + ":" + _allKeys.Keys.Count);
        }

        /**
         * This is a helper function used by the ClientSocket close call to remove the registered keys
         * from this selector. Do not call this function directly. Use the close on the channel.
         * */
        internal void removeChannel(SelectionKey key)
        {
            ChannelLogger.LOG.LogDebug("removeChannel: Removing channel:" + key.channel.channelid + " worker:" + _id);
            doneSelect(key);
            _selectRead.Remove(key);
            _selectWrite.Remove(key);
            _allKeys.Remove(key);
        }

        //add the key for operator in oper to the table. A helper function used
        //by the select function.
        private bool addTo(SelectionKey key, int oper, Hashtable tbl)
        {
            bool exists = false;
            if ((key.opers & oper) == oper)
            {
                if (!tbl.ContainsKey(key))
                    tbl[key] = key;
                else
                    exists = true;
            }

            return exists;
        }

        private IList keysAsList(Hashtable tbl, int read)
        {
            IList lst = new ArrayList();
            if (tbl != null)
            {
                string log = "";
                foreach (SelectionKey key in tbl.Keys)
                {
                    if (!key.readable && !key.writable) //if this key has both the read and write and read is going on do not flag for write till it is done read
                    {
                        if (read == 1)
                            ChannelLogger.LOG.LogDebug("SocketSelector:keysaslist:write present:" + key.writeDataPresent + ":" + _id);
                        if ((read == 0)
                            || ((read == 1) && key.writeDataPresent) //only add when data present
                            || (read == 2))
                        {
                            ClientSocket channel = key.channel as ClientSocket;
                            log = log + ":" + channel.channelid;
                            lst.Add(channel.socket);
                        }
                    }
                }
                ChannelLogger.LOG.LogDebug("Checking for channels: " + log + " : read : " + read + ":count:" + tbl.Keys.Count + ":" + _id);
            }

            return lst;
        }

        /**
         * use this attribute to get and set the global attachment for this selector.
         * */
        public Object attachment { get { return _attachment; } set { _attachment = value; } }

        /**
         * Close the channels currently associated with this selector. It is assumed that the
         * Channel close will deregister the keys and hence deregister of keys is not done directly from here.
         * */
        public void close()
        {
            foreach (SocketSelectionKey key in _allKeys)
            {
                ClientSocket sock = key.channel as ClientSocket;
                sock.close();
            }
        }

        private void addKeysToList(IList frm, IList to, Hashtable lookfor, bool read)
        {
            foreach (object o in frm)
            {
                foreach (SocketSelectionKey key in lookfor.Keys)
                {
                    if (key.Equals(o as Socket))
                    {
                        if (read) 
                            key.canRead = true;
                        else 
                            key.canWrite = true;
                        to.Add(key);
                        break;
                    }
                }
            }
        }

        /**
         * This function calls the select function to determine which of the keys in the registered keys
         * are ready for operation. This also finds if there are any channels in error. 
         * When this function returns > 0 call the selectedkeys attribute to get the keys that are ready 
         * for operation. Once a key is found to be ready for operation, the key is not checked again for 
         * readiness unless the doneSelect is called.
         * This function waits till timeout to check if channel is available. The timeout is specified in
         * microseconds. If none available within the given time, this function returns 0, it does not throw
         * an exception.
         * */
        public int select(int timeout)
        {
            int cnt = 0;

            ChannelLogger.LOG.LogDebug("SocketSelector:select:Select:" + selectedCount + keyCount + ":" + _id);
            //don't check if there even exists one pending select from previous select.
            //otherwise problem. When that select is done, anyways check will come back.
            if ((selectedCount <= 0) && (keyCount > 0))
            {
                IList readlst = keysAsList(_selectRead, 0);
                IList writelst = keysAsList(_selectWrite, 1);
                IList errlst = keysAsList(_allKeys, 2);

                if ((readlst.Count > 0) || (writelst.Count > 0) 
                    || (errlst.Count > 0)) //atleast one socket exists for selecting
                {
                    Socket.Select(readlst, writelst, errlst, timeout);

                    cnt = readlst.Count + writelst.Count + errlst.Count;
                    ChannelLogger.LOG.LogDebug("SocketSelector:select:Selected :" + readlst.Count + 
                        ":" + writelst.Count + ":" + errlst.Count + ":" + _id);
                    addKeysToList(readlst, _selectedKeys, _selectRead, true);
                    addKeysToList(writelst, _selectedKeys, _selectWrite, false);
                }
            }

            return cnt;
        }

        /**
         * Waits infinitely for a channel to be ready for operation.
         * */
        public int select()
        {
            return select(-1);
        }

        /**
         * Returns the selectedkeys by all the previous select calls. This list
         * is not cleared out until the doneSelect is called on the selector.
         * */
        public IList selectedKeys()
        {
            IList ret = new ArrayList(_selectedKeys.Count);
            foreach (object o in _selectedKeys) ret.Add(o);
            return ret;
        }

        /**
         * Call this function to indicate to the selector that the operation readiness on the 
         * key has been completed and the selector can check it again for readiness. This function
         * only removes the key if it is present in the list, if not present, it silently ignores it
         * So, ensure it is present before calling.
         * */
        public void doneSelect(SelectionKey key)
        {
            if (_selectedKeys.Contains(key))
            {
                ChannelLogger.LOG.LogDebug("SocketSelector:doneSelect:removing:" + key.channel.channelid + "worker:" + _id);
                _selectedKeys.Remove(key);
                SocketSelectionKey sock = key as SocketSelectionKey;
                sock.canRead = false;
                sock.canWrite = false;
            }
        }

        /**
         * This attribute returns the number of selectionkeys registered with this selector for
         * checking for operations
         * */
        public int keyCount { get { if (_allKeys != null) return _allKeys.Count; else return 0; } }

        /**
         * This attribute returns the number of keys that are ready for operation and not yet handled
         * by anyone else.
         * */
        public int selectedCount 
        { 
            //ayyoo locks all over the place. may have to change this!!!
            get 
            { 
                if (_selectedKeys != null) 
                    return _selectedKeys.Count; 
                else 
                    return 0; 
            }
        }
    }

    /**
     * This class is used to compare if one selectionkey is the same another
     * This checks for the operation this comparator was initialized for. It checks
     * if both the keys handle the same operation as this comparator and if the channels
     * on the two keys are  the same.
     * */
    public class SelectionKeyComparator : IEqualityComparer
    {
        int _operation;             //The operation to compare for this comparator.

        /**
         * Purely for internal use. Do not use it outside anywhere. Will not be supported.
         * */
        internal SelectionKeyComparator(int operation)
        {
            _operation = operation;
        }

        /**
         * Can compare between two SelectionKey objects and a selectionkey object 
         * and a socket. it returns true if the socket passed in is handled by the
         * selectionkey passed in.
         * */
        public new bool Equals(object x, object y)
        {
            bool ret = false;
            if ((x is SelectionKey) && (y is SelectionKey))
            {
                SelectionKey key = x as SelectionKey;
                SelectionKey key2 = y as SelectionKey;
                ret = key.channel.Equals(key2.channel);
                //if it supports the operation then we say it is equal, else not.
                ret = ret && ((key.opers & _operation) == _operation);
                ret = ret && ((key2.opers & _operation) == _operation);
            }
            else if ((x is SelectionKey) && (y is Socket))
            {
                SelectionKey key = x as SelectionKey;
                Socket key2 = y as Socket;
                ret = key.channel.Equals(key2);
                //if it supports the operation then we say it is equal, else not.
                ret = ret && ((key.opers & _operation) == _operation);
            }

            return ret;
        }

        /**
         * Just redirects to the super hashcode computation.
         * */
        public int GetHashCode(object o)
        {
            return o.GetHashCode();
        }
    }

    /**
     * Implementation of the SelectionKey for the SocketSelectableChannel.
     * */
    public class SocketSelectionKey : SelectionKey
    {
        SelectableChannel _channel;         //Channel for this selection key. Has to be a ClientSocket
        int _operations;                    //Operations that are required to be performed on this channel
        Selector _selector;                 //The selector that will be used with this key.

        bool _hasDataToRead = false;        //has data to be read on channel
        bool _hasDataToWrite = false;       //can write data to channel

        /**
         * Cannot be instantiated outside this module. To instantiate this object call the 
         * register function on the clientSocket class. This class instantiates a key for
         * the given channel, registered to a given selector for the operations indicated.
         * */
        internal SocketSelectionKey(SelectableChannel channel, Selector selector, int opers)
        {
            _channel = channel;
            _operations = opers;
            _selector = selector;
        }

        //attribute to be set if key can be read
        internal bool canRead { set { _hasDataToRead = value; } }

        //attribute to set if key can be written to.
        internal bool canWrite { set { _hasDataToWrite = value; } }

        
        public bool readable { get { return _hasDataToRead; } }     //Can read from channel
        public bool writable { get { return _hasDataToWrite; } }    //Can write to channel
        public bool valid { get { return (_channel != null); } }    //is key valid and channel valid
        public int opers { get { return _operations; } set { _operations = value; } }   //the operations on the channel
        public Selector selector { get { return _selector; } }      //selector for this key
        public SelectableChannel channel { get { return _channel; } }   //channel for this key.
        public bool writeDataPresent { get { return ((ClientSocket)_channel).writePresent; } } //if data is present to be written

        public override bool Equals(object y)
        {
            if (y is SelectionKey)
            {
                SelectionKey key2 = y as SelectionKey;
                bool ret = (key2.channel.Equals(_channel));
                ret = (ret && (key2.opers == _operations));
                return ret;
            }
            else if (y is Socket)
            {
                Socket key2 = y as Socket;
                bool ret = (_channel.Equals(key2));
                return ret;
            }

            return false;
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hash = 17;
                hash = (hash * 23) + _channel.GetHashCode();
                hash = (hash * 23) + _operations.GetHashCode();
                ChannelLogger.LOG.LogDebug("HashCode is: " + hash);
                return hash;
            }
        }
    }

    /**
     * This class is returned when the serversocket accepts a connection and creates a new
     * connected socket. This is a selectable channel and selector functions can be used
     * on this channel.
     * */
    public class ClientSocket : SelectableChannel
    {
        Socket _socket;                                         //accepted socket
        ServerSocket _serverSocket;                             //the server that accepted this socket
        AutoResetEvent _acceptWait = new AutoResetEvent(false); //object to wait to indicate this object is valid
        bool _timedout;                                         //timeout period to wait till this object is discarded
        SocketSelector _selector;                               //the selector that will scan this object
        SelectionKey _key;                                      //the key for operating in this channel
        object _attachment;                                     //attachment to this client socket that can be easily retrieved
        volatile int _writePresent;                             //if data is present to be written. is volatile to be thread safe
        int _channelid;                                         //for debugging

        /**
         * Will be constructed internally by the server socket. Cannot be instantiated 
         * outside.
         * */
        internal ClientSocket(ServerSocket server)
        {
            _serverSocket = server;
        }

        //wait for the object to be valid
        internal void Wait()
        {
            _acceptWait.WaitOne();
        }

        //wakeup the wait.
        internal void Wakeup()
        {
            _acceptWait.Set();
        }

        //a foolish hack due to incapacity of microsoft to give
        //me anything certain. Agreed it is a repulsive thing I don't like to
        //do, but have to do, no choice.
        public bool stillConnected()
        {
            bool conn = _socket.Poll(100, SelectMode.SelectRead);
            int bytes = _socket.Available;
            if (conn && (bytes == 0))
                return false;
            else
                return true;
        }


        //attribute to indicate that this object timedout and is not valid
        internal bool timedout { get { return _timedout; } set { _timedout = value; } }

        //setter to set the accepted socket
        internal Socket acceptedsocket { set { _socket = value; } }

        //the socket for this object
        public Socket socket { get { return _socket; } }
        //the serversocket that created this object
        public ServerSocket serversocket { get { return _serverSocket; } }
        //access for debug
        public int channelid { get { return _channelid; } set { _channelid = value; } }
        public override bool Equals(object sock) 
        {
            if (sock is ClientSocket)
            {
                ClientSocket clnt = sock as ClientSocket;
                return clnt.socket.Equals(_socket);
            }
            else if (sock is Socket)
            {
                Socket s = sock as Socket;
                return _socket.Equals(s);
            }

            return false;
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int hash = 17;
                hash = (hash * 23) + _socket.GetHashCode();
                hash = (hash * 23) + _serverSocket.GetHashCode();
                return hash;
            }
        }

        //register into the selector for the operations.
        public void register(Selector selector, int oper)
        {
            register(selector, oper, null);
        }

        /**
         * Creates a SocketSelectionKey for the given operation and attachment and
         * registers into the selector passed.
         * */
        public void register(Selector selector, int oper, object attachment)
        {
            if ((_selector == null) && (selector is SocketSelector))
            {
                ChannelLogger.LOG.LogDebug("register:Registering Channel:" + this.channelid);
                _key = new SocketSelectionKey(this, selector, oper);
                _selector = selector as SocketSelector;
                _selector.attachment = attachment;
                _selector.addChannel(_key);
            }
            else
                throw new Exception("Cannot register.");
        }

        //returns true if this object is already registered.
        public bool registered
        {
            get { return (_selector != null); }
        }

        //key that allows for select for this object
        public SelectionKey key
        {
            get { return _key; }
        }

        //Close this socket and remove the key from the selector to scan
        public void close()
        {
            if (_selector != null) //deregister from the selector
                _selector.removeChannel(_key);
            if (_socket.Connected)
                _socket.Close();
        }

        //attribute to set and retrieve the attachment to this object
        public object attachment { get { return _attachment; } set { _attachment = value; } }

        //attribute to set and get if data is present
        public bool writePresent 
        { 
            get 
            { 
                return (_writePresent == 1); 
            } 
            set 
            {
                int val = value ? 1 : 0;
                _writePresent = val;
                /*int oldval = _writePresent; //this is a time consuming process, better to just lock
                int compared = Interlocked.CompareExchange(ref _writePresent, val, oldval);
                //threadsafe write
                while (compared != oldval)
                {
                    //try again, possible another thread did something?
                    oldval = _writePresent;
                    compared = Interlocked.CompareExchange(ref _writePresent, val, oldval);
                }*/
                ChannelLogger.LOG.LogDebug("ClientSocket:writePresent:marked" + val);
            } 
        }
    }

    /**
     * The server socket implementation that listens on a given end point and returns a ClientSocket
     * object for every accepted connection.
     * */
    public class ServerSocket
    {
        const int DEFAULT_BACKLOG = 100;        //The default backlog that will be allowed on the socket
        Socket _socket;                         //the real socket on which the connections are waited for
        IPEndPoint _endpoint;                   //The endpoint at which the connections r waited for
        IPHostEntry _host;                      //the host of the current server
        int _backlog;                           //the backlog that will be allowed on the socket
        bool _listening = false;                //if the server has started listening for conns.

        //helper function to create sockets at the given port and 
        //with the given backlog. This checks if the port is valid
        //and if it is also binds the socket to the given port.
        private void createsocket(int port, int backlog)
        {
            _host = Dns.GetHostEntry(Dns.GetHostName());
            AddressFamily fmly = AddressFamily.InterNetwork;
            if (port > 0) //valid port
            {
                _endpoint = new IPEndPoint(_host.AddressList[0], port);
                fmly = _endpoint.Address.AddressFamily;
            }
            _socket = new Socket(fmly, SocketType.Stream, ProtocolType.Tcp);
            _backlog = backlog;

            if (_endpoint != null)
            {
                //bind already
                _socket.Bind(_endpoint);
            }
        }

        //default constructor where no port is bound
        public ServerSocket()
        {
            createsocket(-1, DEFAULT_BACKLOG); //use default values and create an unbound socket
        }

        //creates and binds the socket to the port given
        public ServerSocket(int port)
        {
            createsocket(port, DEFAULT_BACKLOG);
        }

        //creates and binds the socket to the port given with the backlog specified.
        public ServerSocket(int port, int backlog)
        {
            createsocket(port, backlog);
        }

        //socket on which this server is operating
        public Socket socket { get { return _socket; } }

        //bind this server to the given port. If the server is already bound, this will throw an exception
        public void bind(int port)
        {
            if (_socket.IsBound && (_endpoint != null))
            {
                throw new Exception("Already Bound. Cannot bind again");
            }

            _endpoint = new IPEndPoint(_host.AddressList[0], port);
            _socket.Bind(_endpoint);
        }

        //this function will listen if not already listening and wait for one connection to arrive 
        //at the socket before returning the ClientSocket. If no connections arrived within the timeout
        //period this will throw an timed out exception.
        public ClientSocket accept(int timeout)
        {
            if (!_listening)
            {
                if (_endpoint != null)
                    _socket.Listen(_backlog);
                else
                    throw new Exception("Socket not bound. Cannot accept connections.");
            }

            ClientSocket client = new ClientSocket(this);
            _socket.BeginAccept(new AsyncCallback(acceptConnection), client);
            Timer acceptime = new Timer(new TimerCallback(acceptTimeout), client,
                timeout, timeout);
            client.Wait();
            acceptime.Dispose();
            if (client.timedout)
                throw new Exception("Accept timed out no connections");

            return client;
        }

        //helper function to callback during accept. Called  by the microsoft socket library.
        private void acceptConnection(IAsyncResult ar)
        {
            ClientSocket client = (ClientSocket)ar.AsyncState;
            Socket svrsock = client.serversocket._socket;
            client.acceptedsocket = svrsock.EndAccept(ar);
            client.Wakeup();
        }

        //helper function to timeout if no connections are received within the timeout 
        //period set.
        private void acceptTimeout(object eventState)
        {
            ClientSocket sock = (ClientSocket)eventState;
            sock.timedout = true;
            sock.Wakeup();
        }

        //close this server.
        public void close()
        {
            _socket.Close();
        }
    }
}
